/**
 * 
 */
package truerefactor.refactor;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import truerefactor.graph.Accessibility;
import truerefactor.graph.AttributeNode;
import truerefactor.graph.ClassNode;
import truerefactor.graph.CodeGraph;
import truerefactor.graph.CodeNode;
import truerefactor.graph.Edge;
import truerefactor.graph.EdgeType;
import truerefactor.graph.MethodNode;
import truerefactor.graph.StatementNode;
import truerefactor.graph.StatementType;
import truerefactor.graph.TypePair;

/**
 * @author Isaac
 */
public class ReplaceMethodWithMethodObject extends Refactoring {

    /**
     * 
     */
    private static int callCount = 0;

    /**
     * 
     */
    public ReplaceMethodWithMethodObject()
    {
        this.shortName = "RMWMO";
        this.name = "Replace Method with Method Object";
        this.description = "";
    }

    /*
     * (non-Javadoc)
     * @see
     * truerefactor.refactor.Refactoring#execute(truerefactor.graph.CodeGraph )
     */
    @Override
    public void execute(CodeGraph graph)
    {
        MethodNode method = (MethodNode) operands.get(0);
        // 1. Create a new class, name it after the method
        String identifier = method.getIdentifier();
        identifier.replace(identifier.charAt(0), Character.toUpperCase(identifier.charAt(0)));
        ClassNode newClass = new ClassNode(identifier, "");
        newClass.setPackage(method.getParentClass().getPackage());

        // 2. Give the new class a final field for the object that hosted the
        // original method (the source object) and a field for each temporary
        // variable and each parameter in the method
        List<AttributeNode> tempFields = extractTemps(graph, method, newClass);
        List<AttributeNode> paramFields = extractParams(graph, method, newClass);
        AttributeNode source = createSourceAttr(graph, method, newClass);

        // 3. Give the new class a constructor that takes the source object and
        // each parameter
        MethodNode constructor = createConstructor(graph, method.getParentClass(), newClass, identifier, paramFields);

        // 4. Give the new class a method named "compute"
        // 5. Copy the body of the original method into compute. Use the source
        // object field for any invocations of methods on the original object
        MethodNode compute = createCompute(graph, newClass, method, source, tempFields);

        // 6. Replace the old method with one that creates the new object and
        // calls compute
        replaceMethodCalls(graph, method, newClass, compute);
    }

    /**
     * 
     * @param graph
     * @param method
     * @param newClass
     * @return
     */
    private List<AttributeNode> extractTemps(CodeGraph graph, MethodNode method, ClassNode newClass)
    {
        List<AttributeNode> retVals = new ArrayList<AttributeNode>();
        // 2. Give the new class a final field for the object that hosted the
        // original method (the source object) and a field for each temporary
        // variable and each parameter in the method

        HashMap<String, StatementNode> temps = method.findTempVars();
        for (String varName : temps.keySet())
        {
            retVals.add(createAttribute(graph, newClass, varName, temps.get(varName)));
        }

        return retVals;
    }

    /**
     * 
     * @param graph
     * @param method
     * @param newClass
     * @return
     */
    private List<AttributeNode> extractParams(CodeGraph graph, MethodNode method, ClassNode newClass)
    {
        List<AttributeNode> retVals = new ArrayList<AttributeNode>();
        // 2. Give the new class a final field for the object that hosted the
        // original method (the source object) and a field for each temporary
        // variable and each parameter in the method
        List<String> params = method.getParameters();
        for (String param : params)
        {
            TypePair tp = method.getParamInfo(param);
            AttributeNode attr = new AttributeNode(newClass, param, "");
            attr.setPrimitive(tp.isPrimitive());
            attr.setAccessibility(Accessibility.Private);
            newClass.addAttribute(attr);
            graph.insert(attr);
            graph.insert(new Edge(newClass, attr, EdgeType.Attribute));
            retVals.add(attr);
        }

        return retVals;
    }

    /**
     * 
     * @param graph
     * @param method
     * @param newClass
     * @return
     */
    private AttributeNode createSourceAttr(CodeGraph graph, MethodNode method, ClassNode newClass)
    {
        // 2. Give the new class a final field for the object that hosted the
        // original method (the source object) and a field for each temporary
        // variable and each parameter in the method

        newClass.addAssociatedClass(method.getParentClass());
        AttributeNode newAttr = new AttributeNode(newClass, method.getParentClass().getClassName().toLowerCase(), "");
        callCount++;
        newAttr.setFinal(true);
        newClass.addAttribute(newAttr);
        graph.insert(newAttr);
        graph.insert(new Edge(newClass, newAttr, EdgeType.Attribute));

        return newAttr;
    }

    /**
     * 
     * @param graph
     * @param source
     * @param newClass
     * @param identifier
     * @param paramFields
     * @return
     */
    private MethodNode createConstructor(CodeGraph graph, ClassNode source, ClassNode newClass, String identifier,
            List<AttributeNode> paramFields)
    {
        // 3. Give the new class a constructor that takes the source object and
        // each parameter
        MethodNode constructor = new MethodNode(identifier, "");
        constructor.setReturnType("");
        constructor.setReturnPrimitive(false);
        constructor.setAbstract(false);
        constructor.setAccessiblity(Accessibility.Public);
        constructor.setParentClass(newClass);
        newClass.addMethod(constructor);
        graph.insert(constructor);
        graph.insert(new Edge(newClass, constructor, EdgeType.Method));

        // Create Parameters for the constructor and fill in the statement graph
        String sourceAttrID = source.getClassName().toLowerCase();
        constructor.addParameter(sourceAttrID, source.getClassName(), false);
        StatementNode last = new StatementNode(constructor, StatementType.EXPRESSION, "this." + sourceAttrID + " = "
                + sourceAttrID + ";");
        constructor.getStatementGraph().insert(last);
        constructor.getStatements().add(last);

        for (AttributeNode node : paramFields)
        {
            StatementNode next = new StatementNode(constructor, StatementType.EXPRESSION, "this."
                    + node.getIdentifier() + " = " + node.getIdentifier() + ";");
            constructor.addParameter(node.getIdentifier(), node.getType(), node.isPrimitive());
            constructor.getStatementGraph().insert(next);
            constructor.getStatementGraph().insert(new Edge(last, next, EdgeType.Statement));
        }

        return constructor;
    }

    /**
     * 
     * @param graph
     * @param newClass
     * @param method
     * @param source
     * @param tempFields
     * @return
     */
    private MethodNode createCompute(CodeGraph graph, ClassNode newClass, MethodNode method, AttributeNode source,
            List<AttributeNode> tempFields)
    {
        // 4. Give the new class a method named "compute"
        MethodNode compute = new MethodNode("compute", "", Accessibility.Public);
        compute.setReturnType(method.getReturnType());
        compute.setReturnPrimitive(method.isReturnPrimitive());
        compute.setParentClass(newClass);
        newClass.addMethod(compute);
        graph.insert(compute);
        graph.insert(new Edge(newClass, compute, EdgeType.Method));

        // 5. Copy the body of the original method into compute. Use the source
        // object field for any invocations of methods on the original object
        graph.cloneStatementGraph(compute, method.getStatementGraph());

        HashMap<String, StatementNode> tempStmts = compute.findTempVars();
        for (String s : tempStmts.keySet())
        {
            for (AttributeNode node : tempFields)
            {
                if (node.getIdentifier().trim().equals(s.trim()))
                {
                    compute.getStatementGraph().remove(tempStmts.get(s));
                    compute.getStatements().remove(tempStmts.get(s));
                }
            }
        }

        return compute;
    }

    /**
     * 
     * @param graph
     * @param method
     * @param newClass
     * @param compute
     */
    private void replaceMethodCalls(CodeGraph graph, MethodNode method, ClassNode newClass, MethodNode compute)
    {
        // 6. Replace the old method with one that creates the new object and
        // calls compute
        List<MethodNode> assocCalls = method.getAssociatedMethods();
        List<MethodNode> otherCalls = method.getOtherMethods();

        // FIXME: Extract the following code out to its own method
        fixCallingCode(graph, newClass, assocCalls, method);
        fixCallingCode(graph, newClass, otherCalls, method);
    }
    
    /**
     * 
     * @param graph
     * @param newClass
     * @param callList
     * @param method
     */
    private void fixCallingCode(CodeGraph graph, ClassNode newClass, List<MethodNode> callList, MethodNode method) {
        List<StatementNode> calls = new ArrayList<StatementNode>();
        for (MethodNode call : callList)
        {
            for (CodeNode stmt : call.getStatements())
            {
                if (stmt instanceof StatementNode)
                {
                    if (stmt.getCode().contains(method.getIdentifier()))
                        calls.add((StatementNode) stmt);
                }
            }

            int count = 0;
            for (StatementNode stmt : calls)
            {
                StatementNode newObjStmt = new StatementNode(call, StatementType.TYPE_DECLARATION, newClass.getClassName() + " newInstance" + count + " = new " + newClass.getClassName() + "();");
                StatementNode computeStmt = new StatementNode(call, StatementType.EXPRESSION, "newInstance" + count + ".compute();");
                count++;
                
                int index = call.getStatements().indexOf(stmt);
                call.getStatements().add(index, newObjStmt);
                call.getStatements().add(index + 1, computeStmt);
                call.getStatements().remove(stmt);
                
                call.getStatementGraph().insert(newObjStmt);
                call.getStatementGraph().insert(computeStmt);
                call.getStatementGraph().insert(new Edge(newObjStmt, computeStmt, EdgeType.Statement));
                
                for (CodeNode node : call.getStatementGraph().getAdjacencies(stmt))
                {
                    // TODO: remove all edges that link to call and replace them with edges that link to newObjStmt;
                    call.getStatementGraph().remove(call.getStatementGraph().getEdge(stmt, node, EdgeType.Statement));
                }
                
                call.getStatementGraph().remove(call);
            }
            
            if (method.getParentClass().equals(call.getParentClass())) {
                method.removeAssociatedMethod(call);
                call.removeAssociatedMethod(method);
                graph.insert(new Edge(call.getParentClass(), newClass, EdgeType.UseDependency));
            } else {
                method.removeOtherMethod(call);
                call.removeOtherMethod(method);
                graph.remove(graph.getEdge(call.getParentClass(), method.getParentClass(), EdgeType.UseDependency));
                graph.remove(graph.getEdge(call.getParentClass(), method.getParentClass(), EdgeType.Association));
                graph.remove(graph.getEdge(method.getParentClass(), call.getParentClass(), EdgeType.UseDependency));
                graph.remove(graph.getEdge(method.getParentClass(), call.getParentClass(), EdgeType.Association));
                graph.insert(new Edge(call.getParentClass(), newClass, EdgeType.UseDependency));
            }
        }
    }

    /**
     * 
     * @param graph
     * @param newClass
     * @param varName
     * @param stmt
     * @return
     */
    private AttributeNode createAttribute(CodeGraph graph, ClassNode newClass, String varName, StatementNode stmt)
    {
        Set<StatementNode> tempSet = new HashSet<StatementNode>();
        tempSet.add(stmt);
        HashMap<String, TypePair> map = MethodNode.extractParamInfo(tempSet);

        TypePair tp = map.get(varName.trim());

        ClassNode type = null;
        AttributeNode retVal = new AttributeNode(newClass, varName.trim(), stmt.getCode());
        for (ClassNode node : graph.getClasses())
        {
            if (node.getClassName().equals(tp.getType()))
            {
                type = node;
            }
        }

        if (type != null)
        {
            retVal.setAssociatedClass(graph.getClass(tp.getType()));
        }
        retVal.setAccessibility(Accessibility.Private);

        newClass.addAttribute(retVal);
        graph.insert(retVal);
        graph.insert(new Edge(newClass, retVal, EdgeType.Attribute));

        return retVal;
    }
}
